from OpenGL.GL               import *
from OpenGL.GLU              import *
from OpenGL.GLUT             import *

from math                    import sin, cos, pi, exp, floor, sqrt, atan2
from logger                  import log
from timestat                import timestat
from vector                  import dotp, scalarp, vsum, vdiff, mag, magsq,\
                                    rotx, roty, rotz
from time                    import time

moveScale = 5

class Dummy(object): pass

def changesCamera(func):
    """ 
    Decorator which calls onCameraChanged after calling the decorated function
    """
    def wrapper(*args, **kw):
        func(*args, **kw)
        args[0].onCameraChanged() # args[0] is self = Camera object
    
    return wrapper

class Camera(object):
    def __init__(self, parent, state=(0,0,0,0,0)):
        self.canvas = parent
        s = self
        s.x, s.y, s.z, s.t, s.p = state
        s.dt, s.dp, s.dx, s.dy, s.dz = 0, 0, 0, 0, 0
        
        # scaling factors for camera moves
        self.d = 1 
        self.rotFactor = 1.0/150.0
        
        self.MVMatrix = None
        self.PMatrix = None
        self.initVariables()
        self.subscriptions = []
        
    def initVariables(self):
        self.frontVector = lambda s=self: (-sin(s.p + s.dp)*cos(s.t + s.dt),
                                  sin(s.t + s.dt),
                                  -cos(s.p + s.dp)*cos(s.t + s.dt))
        
        self.leftVector  = lambda s=self: (-cos(s.p + s.dp),
                                    0,
                                    sin(s.p + s.dp))
        
        self.upVector    = lambda s=self: (sin(s.p + s.dp)*sin(s.t + s.dt),
                                  cos(s.t + s.dt),
                                  cos(s.p + s.dp)*sin(s.t + s.dt))
        
        self.lastZoomOutTime = self.lastZoomInTime = time()
        self.oldDT = None
        
        

    def subscribe(self, callback):
        if not (callback in self.subscriptions):
            self.subscriptions.append(callback)
            
    def onCameraChanged(self):
        self.runSubscribedCallbacks()
        
    def runSubscribedCallbacks(self):
        for callback in self.subscriptions:
            callback()
        
    def applyRotations(self):
        # Applies camera rotations to current ModelView Matrix
        
        f = self.frontVector()
        u = self.upVector()
        l = self.leftVector()
        fl = mag(f)
        ul = mag(u)
        ll = mag(l)
        
        glMatrixMode(GL_MODELVIEW)
        glRotatef(-(self.t + self.dt)*180/pi, 1, 0, 0)
        glRotatef(-(self.p + self.dp)*180/pi, 0, 1, 0)
        
    def applyTransformation(self):
        # Applies camera transformation, but destroys
        # old MV matrix
        log.debug('Camera state: %4.2f %4.2f %4.2f, %4.2f %4.2f'%(
                 self.x + self.dx, self.y + self.dy, self.z + self.dz,
                 self.t + self.dt, self.p + self.dp))
        
        f = self.frontVector()
        u = self.upVector()
        l = self.leftVector()
        fl = mag(f)
        ul = mag(u)
        ll = mag(l)
        
        #log.debug('Camera front %s, %s'%(str(fl), str(f)))
        #log.debug('Camera up %s, %s'%(str(ul), str(u)))
        #log.debug('Camera left %s, %s'%(str(ll), str(l)))
        glMatrixMode(GL_MODELVIEW)
        #glLoadIdentity()
        glRotatef(-(self.t + self.dt)*180/pi, 1, 0, 0)
        glRotatef(-(self.p + self.dp)*180/pi, 0, 1, 0)
        glTranslatef(-(self.x + self.dx),
                     -(self.y + self.dy),
                     -(self.z + self.dz))
        self.MVMatrix = glGetDoublev(GL_MODELVIEW_MATRIX)
        
    @timestat
    @changesCamera
    def orbit(self, dx, dy, center):
        # orbits around a given center point
        if not center:
            center = 50*self.frontVector()
        c    = center
        s    = self
        dp   = self.rotFactor*dx
        dt   = -self.rotFactor*dy
        cc   = (s.x, s.y, s.z)
        
        # first do the pan around xz plane at the camera's y coordinate
        relc = vdiff(cc, c)
        rxz  = mag((relc[0], 0, relc[2]))
        r    = mag(relc)
        
        
        phi  = atan2(relc[2], relc[0])
        th   = phi + dp
        nc   = (rxz*cos(th), relc[1], rxz*sin(th))
        
        s.dx, s.dy, s.dz = vdiff(nc, relc)
        s.dp = -dp
        
        # now do the tilt in the front / up plane of the camera
        # - find camera position relative to center of rotation for tilt
        f = self.frontVector()
        u = self.upVector()
        l = self.leftVector()
        relc = ((s.x + s.dx) - c[0], (s.y + s.dy) - c[1], (s.z + s.dz) - c[2])
        camP = vsum(scalarp(dotp(f, relc), f), scalarp( dotp(u, relc), u))
        
        # pan back the vector to bring it in yz plane
        tempv1 = roty(camP, -(s.p + s.dp))
        
        # rotate tempv1 wrt x axis by dt angle
        tempv2 = rotx(tempv1, dt)
        
        # pan it again
        tempv3 = roty(tempv2, (s.p + s.dp))
        
        dc = vdiff(tempv3, camP)
        
        # check if tilt is within bounds and y > 0
        if (-pi/2 <= (s.t + dt) <= pi/2) and (s.y + dc[1] > 0):
            s.dx += dc[0]
            s.dy += dc[1]
            s.dz += dc[2]
            s.dt = dt
            
            # save the last good value of tilt that worked
            self.oldDT = dt
        else:
            # use the last good value of tilt in the else block
            
            # rotate tempv1 wrt x axis by dt angle
            if self.oldDT:
                tempv2 = rotx(tempv1, self.oldDT)
                
                # pan it again
                tempv3 = roty(tempv2, (s.p + s.dp))
                
                dc = vdiff(tempv3, camP)
                
                s.dx += dc[0]
                s.dy += dc[1]
                s.dz += dc[2]
                s.dt = self.oldDT
        
    
    @changesCamera
    def zoomIn(self, center):
        ct = time()
        s = self
        cc = (s.x, s.y, s.z)
        if not center:
            dv = scalarp(100, self.frontVector())
        else:
            dv = vdiff(center, cc)
        d = pow((mag(dv)), 0.25)
        
        escale = lambda x: 0.02*exp(-x) + 0.00001
        speed = d*escale(ct - self.lastZoomInTime) + 0.1
        
        offset = scalarp(speed, dv)
        s.x, s.y, s.z = vsum(cc, offset)
        if s.y < 0: s.y = 0
        self.lastZoomInTime = ct
        
    @changesCamera
    def zoomOut(self, center):
        ct = time()
        s = self
        cc = (s.x, s.y, s.z)
        if not center:
            dv = scalarp(100, self.frontVector())
        else:
            dv = vdiff(center, cc)
        d = pow((mag(dv)), 0.25)
        #d = (2*sqrt(d) - 1) if d > 1 else d
        
        escale = lambda x: 0.02*exp(-x) + 0.00001
        speed = d*escale(ct - self.lastZoomOutTime) + 0.1
        
        offset = scalarp(-speed, dv)
        s.x, s.y, s.z = vsum(cc, offset)
        if s.y < 0: s.y = 0
        self.lastZoomOutTime = ct
        
    @changesCamera
    def strafe(self, x, y, d, center, MVMat):
        s = self
        pos = gluUnProject(x, y, d, MVMat, self.PMatrix)
        diff = vdiff(center, pos)
        log.debug('x,y,d = %09.6f, %09.6f, %09.6f'%(x,y,d))
        log.debug('pos   = %09.6f, %09.6f, %09.6f'%(pos))
        log.debug('diff  = %09.6f, %09.6f, %09.6f'%(diff))
        s.dx, s.dy, s.dz = diff
        if (s.y + s.dy) < 0:
            s.dy = -s.y
    
        
    @changesCamera
    def moveForward(self, d=moveScale):
        vx, vy, vz = self.frontVector()
        log.debug('v Vector = %4.2f, %4.2f, %4.2f,'%(vx, vy, vz))
        self.x += vx*d*self.d
        self.y += vy*d*self.d
        self.z += vz*d*self.d
        if self.y < 0: self.y = 0
        
    @changesCamera
    def moveBackward(self, d=moveScale):
        vx, vy, vz = self.frontVector()
        log.debug('v Vector = %4.2f, %4.2f, %4.2f,'%(vx, vy, vz))
        self.x -= vx*d*self.d
        self.y -= vy*d*self.d
        self.z -= vz*d*self.d
        if self.y < 0: self.y = 0
    
    @changesCamera
    def moveLeft(self, d=moveScale):
        vx, vy, vz = self.leftVector()
        log.debug('v Vector = %4.2f, %4.2f, %4.2f,'%(vx, vy, vz))
        self.x += vx*d*self.d
        self.y += vy*d*self.d
        self.z += vz*d*self.d
    
    @changesCamera
    def moveRight(self, d=moveScale):
        vx, vy, vz = self.leftVector()
        log.debug('v Vector = %4.2f, %4.2f, %4.2f,'%(vx, vy, vz))
        self.x -= vx*d*self.d
        self.y -= vy*d*self.d
        self.z -= vz*d*self.d
    
    @changesCamera
    def moveUp(self, d=moveScale):
        vx, vy, vz = self.upVector()
        log.debug('v Vector = %4.2f, %4.2f, %4.2f,'%(vx, vy, vz))
        self.x += vx*d*self.d
        self.y += vy*d*self.d
        self.z += vz*d*self.d
        if self.y < 0: self.y = 0
    
    @changesCamera
    def moveDown(self, d=moveScale):
        vx, vy, vz = self.upVector()
        log.debug('v Vector = %4.2f, %4.2f, %4.2f,'%(vx, vy, vz))
        self.x -= vx*d*self.d
        self.y -= vy*d*self.d
        self.z -= vz*d*self.d
        if self.y < 0: self.y = 0
    
    @changesCamera
    def rotate(self, dx, dy):
        if -pi/2 <= (self.t - self.rotFactor*dy) <= pi/2:
            self.dt = -self.rotFactor*dy
        self.dp = -self.rotFactor*dx
        log.debug('Rotation: %4.2f %4.2f'%(self.dp, self.dt))
        
    def leftDragEnd(self):
        self.t += self.dt
        self.p += self.dp
        self.p  = self.p - floor(self.p/2/pi)*2*pi
        self.dt = 0
        self.dp = 0
    
    def middleDragEnd(self):
        self.t += self.dt
        self.p += self.dp 
        self.p  = self.p - floor(self.p/2/pi)*2*pi
        self.x += self.dx
        self.y += self.dy
        self.z += self.dz
        self.dt = 0
        self.dp = 0
        self.dx = 0
        self.dy = 0
        self.dz = 0
    
    def rightDragEnd(self):
        self.x += self.dx
        self.y += self.dy
        self.z += self.dz
        self.dx = 0
        self.dy = 0
        self.dz = 0
    
    def incMoveDistance(self):
        self.d *= 1.1
        
    def decMoveDistance(self):
        self.d /= 1.1
        
    @changesCamera
    def resetState(self, state):
        s = self
        s.x, s.y, s.z, s.t, s.p = state
        s.dx, s.dy, s.dz, s.dt, s.dp = (0,0,0,0,0)
        
    def getState(self):
        s = self
        return (s.x + s.dx, s.y + s.dy, s.z + s.dz, s.t + s.dt, s.p + s.dp)

        
        